-- DungeonServiceM
-- created by panyl
-- 地牢服务器模块（各类地牢杂项操作、无法归类的服务）

module("DungeonServiceM", package.seeall);

-- 内部函数声明
local whenBuildingEvent;
local whenLeaveDungeon;
local whenMonsterClear;
local whenOpenGrid;
local whenRespoenAck;
local whenInstanceDisapper;
local whenDungeonAready;
local whenDoBonus;
local whenApplyCombatStatus;
local whenPickUpItem;
local whenCombatRound;

-- 等待消息队列
local queue = {};

-- 正在自动打开剩余格子
local autoOpening = false;

-- 迷宫属性
local attribConfig = {};
local itemStatConfig = {};

-- 开始迷宫回调
local startDungeonCB = {};

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    attribConfig = {};
    -- 转换下迷宫属性信息表
    local dungeon_attrib_CSV = get_dungeon_attrib_CSV();
    for i = 1, #dungeon_attrib_CSV do
        local row = dungeon_attrib_CSV[i];
        attribConfig[row.attrib] = row;
    end

    itemStatConfig = {};
    -- 转换下迷宫内物品统计信息表
    local item_stat_CSV = get_dungeon_item_stat_CSV();
    for i = 1, #item_stat_CSV do
        itemStatConfig[item_stat_CSV[i].class_id] = item_stat_CSV[i];
    end
end

-- 模块初始化操作
function init()
    loadCsv();

    -- 关注离开地牢事件
    EventMgr.removeAll("DungeonServiceM");
    EventMgr.register("DungeonServiceM", event.LEAVE_DUNGEON, whenLeaveDungeon);

    -- 关注开启格子事件
    EventMgr.register("DungeonServiceM", event.OPEN_GRID, whenOpenGrid);
    EventMgr.register("DungeonServiceM", event.PURE_OPEN_GRID, whenOpenGrid);

    -- 关注应答
    EventMgr.register("DungeonServiceM", event.RESPOEN_ACK, whenRespoenAck);

    -- 关注副本建筑消失
    EventMgr.register("DungeonServiceM", event.INSTANCE_DISAPPER, whenInstanceDisapper);

    -- 关注开始新一层地牢事件
    EventMgr.register("DungeonServiceM", event.DUNGEON_AREADY, whenDungeonAready);

    -- 关注奖励事件
    EventMgr.register("DungeonServiceM", event.DO_BONUS, whenDoBonus);

    -- 关注附件状态事件
    EventMgr.register("DungeonServiceM", event.APPLY_COMBAT_STATUS, whenApplyCombatStatus);

    -- 关注通用建筑事件
    EventMgr.register("DungeonServiceM", event.BUILDING_EVENT_DONE, whenBuildingEvent);

    -- 关注通用建筑事件
    EventMgr.register("DungeonServiceM", event.PICK_UP_ITEM, whenPickUpItem);

    -- 回合事件
    EventMgr.register("DungeonServiceM", event.COMBAT_ROUND, whenCombatRound);
end

-- 开始迷宫回调
function handleStartDungeon()
    local arr = table.keys(startDungeonCB);
    table.sort(arr);

    for _, key in ipairs(arr) do
        local f = startDungeonCB[key];
        if type(f) == "function" then
            f();
        end
    end
end

function registerStartDungeonCB(key, f)
    if type(f) ~= "function" then
        return;
    end

    startDungeonCB[key] = f;
end

-- 缓存同步操作消息，如果没有收到服务器的应答就重发
function addWaitSync(cmd, msg, only_one)
    local id = msg.auth_id;

    msg.only_one = only_one;

    queue[id] = { cmd, msg, };

    -- 先发一次
    reSendMsg();

    -- 定时重连
    ScheduleM.createScheme("DungeonServiceM", reSendMsg, SYNC_MSG_REPOST_TIME, true)
end

-- 重发消息
function reSendMsg()
    local keys = table.keys(queue);

    trace("DungeonServiceM", "重发消息，总共有%d条sync消息。", #keys);
    cclog("迷宫消息队列：%o\n", queue);

    -- 如果消息已经空了或者已经离开游戏了
    if not ME.isInGame or (#keys <= 0) then
        -- 移除定时器
        clearMsgQue();

        return;
    end

    -- 如果网络没连接上就不管了
    if not Socket.isConnected() then
        trace("DungeonServiceM", "网络连接已断开！");
        return;
    end

    -- 不能同步
    if not SyncM.canSync() then
        trace("DungeonServiceM", "SyncM模块目前尚不能发送消息！");
        return;
    end

    -- 发送每条同步消息，按照id排序
    table.sort(keys);
    for _, id in pairs(keys) do
        if queue[id] then
            local cmd = queue[id][1];
            local v   = queue[id][2];
            SyncM.addMessage(cmd, v);
        end

        -- 如果是仅发一次，删除
        if queue[id][2].only_one then
            queue[id] = nil;
        end
    end

    SyncM.startSync();
end

function clearMsgQue()
    -- 移除定时器
    ScheduleM.deleteScheme("DungeonServiceM");

    queue = {};
end

-- 开始进入地牢
function startEnterService(dungeonId, extra)
    if getStartTime() then
        -- 事件进行中
        trace("DungeonServiceM", "事件进行中。");
        return false;
    end

    -- 如果迷宫已经开始了
    if DungeonM.getState() == DUNGEON_GAMING then
        trace("DungeonServiceM", "迷宫已经处于开始状态了。");
        return false;
    end

    -- 如果没有该迷宫
    if not DungeonAreaM.query(dungeonId) then
        trace("DungeonServiceM", "迷宫(%d)不存在。", dungeonId);
        return false;
    end

    local keys = table.keys(queue);
    local authId = tonumber(os.time());
    if #keys <= 0 and not SyncM.needSync() then
        -- 确保没有同步消息了
        -- SLIMEC-2599

        Operation.cmd_start_dungeon(dungeonId, authId, extra);
    end

    -- 定时重发
    ScheduleM.createScheme("DungeonServiceMStartTimer", function()
                        local keys = table.keys(queue);
                        if #keys <= 0 and not SyncM.needSync() then
                            Operation.cmd_start_dungeon(dungeonId, authId, extra);
                        end
                    end, 1, true);

    return true;
end

-- 结束请求
function stopEnterService()
    -- 停止循环事件
    ScheduleM.deleteScheme("DungeonServiceMStartTimer")
end

-- 是否在等待进入请求
function getStartTime()
    return ScheduleM.isSchemeAlive("DungeonServiceMStartTimer");
end

-- 升级技能
function upgradeSkill(skillId)
    -- 升级只要给玩家升级就行了
    ME.user:upgradeSkill(skillId);

    -- 刷新属性
    PropM.refreshEffect(ME.user, "skill");
end

-- 学习技能
function learnSkill(skillId)
    -- 玩家学习技能
    --ME.user:learnSkill(skillId);

    -- 装备技能
    local option = table.append(ME.user:getSkillsOption(), { skillId, });
    ME.user.skillOption = option;

    -- 刷新属性
    PropM.refreshEffect(ME.user, "skill");
end

-- 判断是否执行清怪处理
function isMonsterClear()
    -- 先取出所有格子
    local grids = DungeonM.getCurrentDungeon();

    for _, grid in pairs(grids) do
        -- 还有怪物没死
        if grid:isMonster() and
            (not grid.monster.isAudience or not grid.monster:isAudience())
            and not grid.monster:isDead() then
            return false;
        end
    end

    -- 流浪者帐篷还存在
    if DungeonTentM.findTentGrid() and not BossTentM.isHaveBossTent() then
        trace("DungeonServiceM", "流浪者帐篷中的怪物尚未清除，不执行清怪奖励。");
        return false;
    end

    -- 刷怪建筑还存在
    if #MonsterBuildingM.findMonsterBuildingGrids() > 0 then
        trace("DungeonServiceM", "刷怪建筑还有怪物尚未清除，不执行清怪奖励。");
        return false;
    end

    -- 还有转换消除建筑存在
    if TransEliminateM.hasTransEliminate() then
        trace("DungeonServiceM", "转换消除建筑还存在，不执行清怪奖励。");
        return false;
    end

    return true;
end

-- 清怪处理
function monsterClear(noEventFire)
    -- 判断是否执行清怪处理
    if not isMonsterClear() then
        return false;
    end

    -- 奖励探索点
    local value = 0;

    local dungeonId = DungeonM.getDungeonId();
    local dbase = DungeonAreaM.query(dungeonId, "dbase") or {};

    -- 如果不在地牢副本中
    if ME.user.dbase:queryTemp("monster_cleared") ~= DungeonM.currentLayer() and
        not DungeonInstanceM.isInInstance() and not UnrealDungeonM.isInUnreal() and dbase["forbid_monster_clear"] ~= 1 then
        value = FormulaM.invoke("CALC_GAIN_SEARCH_POINT_WHEN_CLEAR", ME.user, DungeonM.currentLayer());

        -- 探索点加成
        local prop = PropM.combine(ME.user, "bonus", "search_point");
        value = PropM.apply(prop, value);

        BonusM.doBonus({ 2, "search_point", value, }, "monster_clear", false);

        -- 获取所有格子
        local curDungeon = DungeonM.getCurrentDungeon();
        for _, grid in pairs(curDungeon) do
            if grid.state == GRID_STATE_COVER then
                grid:open();
            end
        end

        -- 标记怪物全清了
        ME.user.dbase:setTemp("monster_cleared", DungeonM.currentLayer());
    end

    -- 抛出清怪事件，神龙夹层中时强制抛一下
    if UnrealDungeonM.isInUnreal() or value > 0 then
        if type(value) == "number" and value > 0 then
            ME.user.monsterClearedTip = value;
        end
        if not noEventFire then
            EventMgr.fire(event.MONSTER_CLEAR, { ["search_point"] = value, });
        end
    end

    return true;
end

-- 打开剩余未开的格子
function openOneLeftGrid()
    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        if grid:canOpen() and not grid:isOpened() then
            grid:open();
            -- 只打开一个
            return true;
        end
    end

    return false;
end

-- 正在打开剩余格子
function setAutoOpeningFlag(flag)
    autoOpening = flag;
end

-- 获取正在打开剩余格子的状态
function getAutoOpeningFlag(flag)
    return autoOpening;
end

-- 收到服务器应答消息
function whenRespoenAck(id)
    -- 移除等待消息
    clearQueue(id);
end

function clearQueue(id)
    -- 移除等待消息
    queue[id] = nil;
end

whenApplyCombatStatus = function(para)
    local who = para["target"];
    local status = para["status"];
    local statusInfo = CombatStatusM.getStatusInfo(status);

    -- TODO：这里应该改成由trigger_type触发
    -- 如果是中毒
    if "poisoned" == statusInfo.alias then
        local props = PropM.fetchProps(who, "conversion_toxin");
        for _, prop in ipairs(props) do
            PropM.trigger(who, prop[1], prop[2]);
        end

        props = PropM.fetchProps(who, "poisoning_to_attrib");
        for _, prop in ipairs(props) do
            PropM.trigger(who, prop[1], prop[2]);
        end

        props = PropM.fetchProps(who, "poisoning_to_status");
        for _, prop in ipairs(props) do
            PropM.trigger(who, prop[1], prop[2]);
        end
    end

    -- 如果是燃烧
    if "burn" == statusInfo.alias then
        local props = PropM.fetchProps(who, "burning_to_attrib");
        for _, prop in ipairs(props) do
            PropM.trigger(who, prop[1], prop[2]);
        end
    end

    -- 禁锢类状态，附加禁锢抗性
    if statusInfo.durance == 1 then
        local endRound = para["round"];
        if endRound > 0 then
            endRound = endRound + 3;
        end

        local condition = {
            ["id"] = PropM.getPropId("durance_resist"),
            ["end_round"] = endRound,
        };

        if "durance_resist" ~= statusInfo.alias then
            CombatStatusM.applyStatus(who, "durance_resist", condition);
        end
    end
end

-- 奖励
whenDoBonus = function(para)
    -- 如果不在迷宫中
    if not DungeonM.isInDungeon() then
        return;
    end

    -- 如果是物品奖励
    if para[1] == 1 then
        -- TODO: 有些道具需要统计
        local classId = para[2];
        local amount  = para[3];
        if FormulaM.invoke("NEED_COLLECT_ITEM_STAT", classId) then
            -- 统计
            local itemStat = ME.user.dbase:query("item_stat", {});

            ME.user.dbase:set("item_stat", itemStat);

            itemStat[classId] = (itemStat[classId] or 0) + amount;
        end

        -- 有些装备需要统计
        if EquipM.isEquipment(classId) then
            local dbase = EquipM.query(classId, "dbase");
            if type(dbase["gain_stat"]) then
                BuildingBonusM.addFieldStat(dbase["gain_stat"], amount);
            end
        end
    end
end

-- 回合结束
whenCombatRound = function()
    local round = CombatM.getRound();
    local layer = DungeonM.currentLayer();
    local dungeonId = DungeonM.getDungeonId();
    local limitRound = 150 + layer * 2;

    -- 如果是在迷宫夹层
    if UnrealDungeonM.isInUnreal() then
        return;
    end

    --
    local dungeonType = DungeonAreaM.query(dungeonId, "type");
    if "activity" == dungeonType or "sky_instance" == dungeonType then
        return;
    end

    -- 如果是boss层
    if DungeonBossM.isBossLayer(dungeonId, layer, ME.user) then
        limitRound = limitRound * 2;
    end

    EventMgr.fire(event.DUNGEON_COLLAPSE, {["left"] = limitRound - round, });
end

-- 拾取物品
whenPickUpItem = function(para)
    local bonus = para["bonus"];
    local class = para["class"];

    -- 如果不在迷宫中
    if DungeonM.getState() ~= DUNGEON_GAMING then
        return;
    end

    local dungeonId = DungeonM.getDungeonId();
    local dbase = DungeonM.query(class, "dbase") or {};

    if dbase["only_one"] then
        -- 该迷宫仅允许出现一次
        local foreverData = ME.user.dbase:query("no_more_appear_class", {});
        local data = foreverData[dungeonId] or {};

        data[class] = 1;
        foreverData[dungeonId] = data;
        ME.user.dbase:set("no_more_appear_class", foreverData);
    end

    if type(bonus) == "table" and #bonus > 0 then
        if type(bonus[1]) ~= "table" then
            bonus = { bonus, };
        end

        for _, arr in pairs(bonus) do
            -- 是学徒
            if arr[1] == 2 and arr[2] == "idle_worker_num" then
                -- 获取营救的冈布奥学徒数量
                local rescueAmount = arr[3];
                if type(rescueAmount) ~= "number" or rescueAmount <= 0 then
                    break;
                end

                local layer = DungeonM.currentLayer();
                local mod   = DungeonM.getModule(class);

                if mod and mod.rescueRecord then
                    mod.rescueRecord(dungeonId, layer);
                end
            end
        end
    end
end

-- 离开地牢的回调
whenLeaveDungeon = function()
    ItemM.cleanNewItemList();

    ME.user.dbase:delete("dungeon_mixed");

    -- 清除神灯使用记录
    BuffDataM.set("lamp_already_used", 1, 0);

    -- 重置银行兑换次数
    DungeonBankM.resetExchangeTimes();

    -- 清空技能动作序列
    SkillM.clearSequenceList();

    -- 1. 清除地牢属性
    local attribs = FormulaM.invoke("FETCH_DUNGEON_ATTRIBS");
    for _, attrib in pairs(attribs) do
        ME.user.dbase:set(attrib, 0);
    end

    -- 2. 清除卷轴
    local allSpells = ItemM.getItemsByType(ME.user, ITEM_TYPE_SPELL);
    for _, classId in pairs(allSpells) do
        ItemM.updateItem(ME.user, classId, 0);
    end

    -- 3. 清除宝物？
    local allEquips = EquipM.getAllTreasure();
    for _, classId in pairs(allEquips) do
        ItemM.updateItem(ME.user, classId, 0);
    end

    -- 穿戴触发记录全部清空
    ME.user.dbase:delete("wear_equip");

    -- 将神器全部脱下来
    for _, arr in pairs(ME.user.equipments) do
        for _, classId in pairs(arr) do
            if ArtifactM.isArtifact(classId) then
                ItemM.addAmount(ME.user, classId, 1);
            end
        end
    end

    ME.user.equipments = {};

    -- 清除可使用道具
    local properties = PropertyM.getAllProperties();
    local carriableItem = ItemM.getAllCarriableItems();

    for _, classId in pairs(properties) do
        local count = ItemM.getAmount(ME.user, classId);
        if PropertyM.isDungeonProperty(classId) and
            table.indexOf(carriableItem, classId) == -1 and
            count > 0 then
            -- 数量大于0才清除
            ItemM.updateItem(ME.user, classId, 0);

            -- 抛出事件
            EventMgr.fire(event.LEAVE_DUNGEON_CLEAR_ITEM, {["class_id"] = classId, ["count"] = count, });
        end
    end

    -- 道具使用次数
    ME.user.dbase:delete("property_used");

    ME.user.dbase:delete("dungeon_item");
    ME.user.dbase:delete("take_out_item");

    -- 4. 其他一些属性
    ME.user.dbase:delete("holy_revive");
    ME.user.dbase:delete("has_revived");
    ME.user.dbase:delete("hp_to_skill");

    -- 海格力斯变身次数
    ME.user.dbase:delete("trans_times");

    -- 清除宠物的职业信息
    CareerM.clearPetCareer();

    -- 删除属性
    ME.user.dbase:deleteTemp("prop");
    ME.user.dbase:deleteTemp("summon_list");

    -- 删除永久（持续整个迷宫）属性
    ME.user.dbase:delete("forever_prop");

    -- 大地之门的次数记录
    ME.user.dbase:delete("has_back_floor");

    -- 道具统计
    ME.user.dbase:delete("item_stat");

    -- 杀戮程式优化点
    ME.user.dbase:delete("opti_point");

    -- 狮身人面像谜语标记
    ME.user.dbase:delete("sphinx_riddle");

    -- 当前迷宫标记
    ME.user.dbase:delete("current_dungeon");

    -- 达到的最高层数
    ME.user.dbase:delete("max_dungeon_layer");

    ME.user.dbase:deleteTemp("monster_cleared");

    -- 删除迷宫属性
    ME.user.dbase:delete("dungeon_attrib");

    -- 删除boss宝箱备选数据
    ME.user.dbase:delete("boss_box_backup");

    -- 5.清除其他物品
    local otherItems = FormulaM.invoke("DUNGEON_TAKE_BACK_ITEMS");
    if #otherItems > 0 then
        for _, id in pairs(otherItems) do
            ItemM.updateItem(ME.user, id, 0);
        end
    end

    -- 再刷新属性
    PropM.refresh(ME.user);
end

-- 迷宫准备完毕回调
whenDungeonAready = function()
    -- 显示怪物标记
    showMonsterMark();

    -- 一些杂项处理
    ME.user.dbase:deleteTemp("skillTriggerQueue");
end

-- 显示怪物标志
function showMonsterMark()
    -- 如果不是在迷宫中
    if not DungeonM.isInDungeon() or DungeonM.getState() ~= DUNGEON_GAMING then
        return;
    end

    -- 1. 标记怪物
    local prop = PropM.combine(ME.user, "show_monster", 1);
    local count = PropM.apply(prop, 0);
    local arr = {};
    if count > 0 then
        for pos = 1, GRID_SIZE do
            local grid = DungeonM.getGridByPos(pos);

            if grid.MarkMonster then
                count = count - 1;
            elseif not grid:isOpened() and grid:isMonster() and not grid.monster:isDead() then
                table.insert(arr, grid);
            end
        end
    end

    for i = 1, count do
        if #arr <= 0 then
            break;
        end

        local index = math.random(#arr);
        local grid = arr[index];

        -- 移除
        table.remove(arr, index);

        -- 没翻开的才需要标记一下
        if not grid:isOpened() and not grid:isPublicGrid() then
            grid.MarkMonster = true;
        end
    end

    for pos = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(pos);

        -- 没翻开的才需要标记一下
        if not grid:isOpened() and grid.MarkMonster then
            EventMgr.fire(event.MARK_MONSTER, {["pos"] = grid:getPos(),});
        end
    end
end

-- 显示怪物标志
function showMonsterBounty()
    -- 如果不是在迷宫中
    if DungeonM.getState() ~= DUNGEON_GAMING then
        return;
    end

    -- 1. 标记怪物
    local pos;
    for pos = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(pos);

        if not grid:isOpened() and grid:isMonster() then
            if  grid.monster:query("track") then
                EventMgr.fire(event.MARK_MONSTER, {["pos"] = grid:getPos(), ["fileName"] = "track"});
            end
        end
    end
end

-- 显示怪物标志
function showMonsterDragon()
    -- 如果不是在迷宫中
    if not DungeonM.isInDungeon() then
        return;
    end

    -- 1. 标记怪物
    local pos;
    for pos = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(pos);

        if not grid:isOpened() and grid:isMonster() then

            if  grid.monster:query("dragon") then
                EventMgr.fire(event.MARK_MONSTER, {["pos"] = grid:getPos(), ["fileName"] = "dragon"});
            end
        end
    end
end

-- 开始新一层的回调
function enterTrigger()

    local fetchProps = PropM.fetchProps;
    local trigger = PropM.trigger;

    -- 龙珠属性
    local props = fetchProps(ME.user, "dragon");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 赏金猎人属性
    props = fetchProps(ME.user, "track");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 每层地牢获得持续类魔法效果
    props = fetchProps(ME.user, "skill_buff");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 每层地牢获得非持续类魔法效果
    props = fetchProps(ME.user, "skill_buff2");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 每层地牢赋予战斗状态
    props = fetchProps(ME.user, "floor_status");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 进入迷宫增加属性
    props = fetchProps(ME.user, "enter_gain_attrib");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 显示钥匙怪
    props = fetchProps(ME.user, "show_key");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    BonusM.startBonusCollect();

    -- 塔罗牌
    props = fetchProps(ME.user, "tarot");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    BonusM.endBonusCollect();
end

-- 进入下一层的回调
function whenPassFloor()
    -- 先判断下地牢的情况
    -- 未开启无尽模式时，三星评价楼层再往下就是结算了，不能再触发属性
    local layer = DungeonM.currentLayer();
    local dungeonID = DungeonM.getDungeonId();

    if layer >= FormulaM.invoke("CALC_DUNGEON_LAYER_LIMIT", dungeonID) then
        return;
    end

    -- 开始收集奖励
    BonusM.startBonusCollect();

    local fetchProps = PropM.fetchProps;
    local trigger = PropM.trigger;

    -- TODO: 召唤兽的掉落自动拾取，放在enter_do_summon属性中处理
    local props = fetchProps(ME.user, "enter_do_summon");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2], false);
    end

    -- 1. 卷轴奖励
    props = fetchProps(ME.user, "gift");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "spell_bonus");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "spell_bonus2");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "spell_bonus3");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 2. 物品奖励
    props = fetchProps(ME.user, "crystal_bonus");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    local prop = PropM.combine(ME.user, "doraemon", "enter_bonus");
    if prop[3] > 0 then
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 3. 进入下层恢复
    props = fetchProps(ME.user, "stage_recover");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 钢铁之躯
    props = fetchProps(ME.user, "man_of_steel");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "stage_recover2");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "stage_attrib");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- 给召唤兽回复
    props = fetchProps(ME.user, "recover_summon");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "item_per10floor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "item_per5floor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "item_per2floor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "attrib_per10floor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "item_perfloor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "item_per2floor2");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "item_per3floor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "reagent_bonus");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "gain_attrib");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "fixed_layer_bonuses");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "lock_ring");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    props = fetchProps(ME.user, "cannon_perfloor");
    for _, prop in ipairs(props) do
        trigger(ME.user, prop[1], prop[2]);
    end

    -- TODO:龙语魔法特殊处理，这边添加龙血10点
    local dragonMagic = DungeonM.getDungeonMixedField("dragon_magic") or 0;
    if dragonMagic == 1 then
        addDungeonAttrib("dragon_blood", 10);
    end

    BonusM.endBonusCollect();
    local bonusList = BonusM.getBonusCollect();

    -- 记录下开始血量
    local initAttrib = ME.user.dbase:query("dungeon_init_attrib", {});
    initAttrib["curr_hp"] = ME.user:queryAttrib("hp");
    initAttrib["curr_mp"] = ME.user:queryAttrib("mp");
    ME.user.dbase:set("dungeon_init_attrib", initAttrib);

    -- 召唤兽同步下属性
    SummonM.whenEnterNext();

    -- 魔藤
    BineM.nextFloor();

    -- 阿瓦隆巨炮
    AvalonCannonM.whenNextFloor();
end

-- 开格子事件
function whenOpenGrid(para)
    Profiler.funcBegin("whenOpenGrid");

    -- 如果是门，则不用管了
    if para.type == GRID_TYPE_DOOR then
        return;
    end

    -- 记录一下在哪个回合翻开的
    local grid = DungeonM.getGridByPos(para.pos);
    grid.openRound = CombatM.getRound();

    local monster = grid.monster;
    if monster and not monster:isDead() then
        -- 遍历所有出现触发的属性
        for _, propId in ipairs(PropM.getTriggerProps("appear")) do
            local props = PropM.fetchProps(monster, propId);
            for _, prop in ipairs(props) do
                PropM.trigger(monster, prop[1], prop[2]);
            end
        end
    end

    -- 奖励一点探索点
    local bonus = FormulaM.invoke("CALC_OPEN_GRID_BONUS", para.pos);

    if bonus and bonus[3] > 0 then
        BonusM.doBonus(bonus, "open_grid");
    end

    Profiler.funcEnd("whenOpenGrid");
end

-- 显示公开的格子
function showPublicGrid()
    -- 判断是否默认开启所有的格子
    local grid;
    local grids = DungeonM.getCurrentDungeon();
    if DungeonM.isOpenAllGridsWhenStart() then
        DungeonM.openAllGrids();

        for _, grid in pairs(grids) do
            if not grid.openRound then
                grid.openRound = 0;
            end
        end
    else
        for _, grid in pairs(grids) do
            if  grid:isPublicGrid() then
                -- 需要打开格子
                grid:open(true);

                -- TODO: 这里设置为0（认为是0回合翻开的）
                grid.openRound = 0;
            end
        end
    end
end

-- 副本建筑消失
function whenInstanceDisapper(args)
    local instanceId = args.id;
    -- 如果是流浪者帐篷副本，尝试执行清怪奖励
    if DungeonTentM.isTentInstance(instanceId) then
        -- 如果是boss层的流浪者帐篷，那么return
        if BossTentM.isHaveBossTent() then
            return;
        end

        monsterClear();
        DungeonTentM.tryLevelUpThirdHand();
    end
end

-- 获取物品统计
function getItemStat(classId)
    local itemStat = ME.user.dbase:query("item_stat", {});

    return itemStat[classId] or 0;
end

-- 查询迷宫属性
function queryDungeonAttrib(attrib)
    local dungeonAttrib = ME.user.dbase:query("dungeon_attrib", {});

    if dungeonAttrib[attrib] then
        return dungeonAttrib[attrib];
    end

    return ME.user:queryAttrib(attrib);
end

-- 查询属性信息
function getDungeonAttribInfo(attrib, path)
    local m = attribConfig[attrib];

    if not m or not path then
        return m;
    end

    return m[path];
end

-- 增加迷宫属性，需要在dungeon_attrib.csv配置了的属性才能增加
function addDungeonAttrib(attrib, value)
    -- 如果没有配置此属性
    if not attribConfig[attrib] then
        return false;
    end

    local dungeonAttrib = ME.user.dbase:query("dungeon_attrib", {});
    local oldValue = (dungeonAttrib[attrib] or 0);
    local v = oldValue + value;

    if NUMBER_INFINITY ~= attribConfig[attrib]["limit"] and
        v > attribConfig[attrib]["limit"] then
        v = attribConfig[attrib]["limit"];
    end

    dungeonAttrib[attrib] = v;

    ME.user.dbase:set("dungeon_attrib", dungeonAttrib);

    local params = {attrib = attrib,  from = oldValue, to = v };

    if DungeonM.getState() == DUNGEON_LOADING then
        -- 暂存事件信息，如果被处理了，暂存信息会被清掉
        ME.user.dbase:setTemp("temp_dungeon_attrib", attrib, params);
    else
        EventMgr.fire(event.DUNGEON_ATTRIB_CHANGE, params);
    end

    -- 尝试转换形态
    PropM.trigger(ME.user, "queen_trans", 1);

    return true;
end

-- 扣除迷宫属性
function costDungeonAttrib(m)
    -- 先检查一遍
    local dungeonAttrib = ME.user.dbase:query("dungeon_attrib", {});

    for attrib, value in pairs(m) do
        if queryDungeonAttrib(attrib) < value then
            return false;
        end
    end

    -- 扣除
    for attrib, value in pairs(m) do
        if dungeonAttrib[attrib] then
            local oldValue = dungeonAttrib[attrib];

            dungeonAttrib[attrib] = dungeonAttrib[attrib] - value;

            local params = {attrib = attrib,  from = oldValue, to = dungeonAttrib[attrib] };

            if DungeonM.getState() == DUNGEON_LOADING then
                -- 暂存事件信息，如果被处理了，暂存信息会被清掉
                ME.user.dbase:setTemp("temp_dungeon_attrib", attrib, params);
            else
                EventMgr.fire(event.DUNGEON_ATTRIB_CHANGE, params);
            end
        else
            -- 扣除玩家属性
            ME.user:costAttrib(attrib, value);
        end
    end

    return true;
end

-- 扣除迷宫内物品
function costDungeonItem(classId, amount)
    -- 不可能为负数
    assert(amount >= 0);

    if DungeonM.isInDungeon() then
        if ItemM.isCarriableItem(classId) then
            local dungeonItem = ME.user.dbase:query("dungeon_item", {});

            -- 扣除数量
            dungeonItem[classId] = dungeonItem[classId] - amount;
            ME.user.dbase:set("dungeon_item", dungeonItem);
        else
            ItemM.costAmount(ME.user, classId, amount);
        end
    end

    return true;
end

-- 获取迷宫中物品数量
function getDungeonItemAmount(classId)
    if ItemM.isCarriableItem(classId) then
        -- 可携带进迷宫的道具
        local dungeonItem = ME.user.dbase:query("dungeon_item", {});
        return dungeonItem[classId] or 0;
    end

    return ItemM.getAmount(ME.user, classId);
end

-- 尝试触发自动施法属性
function tryAutoCast()
   if ME.user:isDead() then
        trace("DungeonServiceM", "玩家已经死亡，不能触发自动施法属性。");
        return false, false;
    end

    local done = false;
    local props = PropM.fetchProps(ME.user, "auto_cast2");
    for _, prop in ipairs(props) do
        PropM.trigger(ME.user, prop[1], prop[2]);

        done = true;
    end

    -- 添加同步操作缓存
    if done then
        DungeonM.addAction({ ["cmd"] = "prop_auto_cast2",  });
    end

    -- action已经添加了
    return done, true;
end

-- 通用建筑事件回调
function whenBuildingEvent(args)
    local pos = args.pos;
    local grid = DungeonM.getGridByPos(pos);
    if not grid then
        return;
    end

    -- 尝试触发属性rummage_soul
    local props = PropM.fetchProps(ME.user, "rummage_soul");
    for _, prop in ipairs(props) do
        local para = { ["grid"] = grid, };
        PropM.trigger(ME.user, prop[1], prop[2], para);
    end
end

-- 是否需要统计的物品（一次迷宫）
function isStatItem(classId)
    if itemStatConfig[classId] then
        return true;
    else
        return false;
    end
end

-- 统计物品数量是否已达上限
function isStatItemLimit(classId)
    local info = itemStatConfig[classId];
    if not info then
        return false;
    end

    -- 未配置上限
    if type(info["limit"]) ~= 'number' or info["limit"] <= 0 then
        return false;
    end

    return getItemStat(classId) >= info["limit"];
end
